#!/bin/sh
#
# american fuzzy lop++ - status check tool
# ----------------------------------------
#
# Originally written by Michal Zalewski
#
# Copyright 2015 Google Inc. All rights reserved.
# Copyright 2019-2024 AFLplusplus Project. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#   https://www.apache.org/licenses/LICENSE-2.0
#
# This tool summarizes the status of any locally-running synchronized
# instances of afl-fuzz.
#

test "$1" = "-h" -o "$1" = "-hh" && {
  echo "$0 status check tool for afl-fuzz by Michal Zalewski"
  echo
  echo "Usage: $0 [-s] [-d] afl_output_directory"
  echo
  echo Options:
  echo "  -d  -  include dead fuzzer stats"
  echo "  -m  -  just show minimal stats"
  echo "  -n  -  no color output"
  echo "  -s  -  skip details and output summary results only"
  echo
  exit 1
}

unset MINIMAL_ONLY
unset NO_COLOR
unset PROCESS_DEAD
unset SUMMARY_ONLY
unset RED
unset GREEN
unset YELLOW
unset BLUE
unset NC
unset RESET

if [ -z "$TERM" ]; then export TERM=vt220; fi

while [ "$1" = "-d" -o "$1" = "-m"  -o "$1" = "-n"  -o "$1" = "-s" ]; do
  
  if [ "$1" = "-d" ]; then
    PROCESS_DEAD=1
  fi
  
  if [ "$1" = "-m" ]; then
    MINIMAL_ONLY=1
  fi
  
  if [ "$1" = "-n" ]; then
    NO_COLOR=1
  fi
  
  if [ "$1" = "-s" ]; then
    SUMMARY_ONLY=1
  fi
  
  shift
  
done

DIR="$1"

if [ "$DIR" = "" -o "$DIR" = "-h" -o "$DIR" = "--help" ]; then
  
  echo "$0 status check tool for afl-fuzz by Michal Zalewski" 1>&2
  echo 1>&2
  echo "Usage: $0 [-d] [-m] [-n] [-s] afl_output_directory" 1>&2
  echo 1>&2
  echo Options: 1>&2
  echo "  -d  -  include dead fuzzer stats" 1>&2
  echo "  -m  -  just show minimal stats" 1>&2
  echo "  -n  -  no color output" 1>&2
  echo "  -s  -  skip details and output summary results only" 1>&2
  echo 1>&2
  exit 1
  
fi

if [ -z "$MINIMAL_ONLY" ]; then
  echo "$0 status check tool for afl-fuzz by Michal Zalewski"
  echo
fi

cd "$DIR" || exit 1

if [ -d queue ]; then
  
  echo "[-] Error: parameter is an individual output directory, not a sync dir." 1>&2
  exit 1
  
fi

BC=`which bc 2>/dev/null`
FUSER=`which fuser 2>/dev/null`

if [ -z "$NO_COLOR" ]; then
  RED=`tput setaf 9 1 1 2>/dev/null`
  GREEN=`tput setaf 2 1 1 2>/dev/null`
  BLUE=`tput setaf 4 1 1 2>/dev/null`
  YELLOW=`tput setaf 11 1 1 2>/dev/null`
  NC=`tput sgr0`
  RESET="$NC"
fi

PLATFORM=`uname -s`
#if [ "$PLATFORM" = "Linux" ] ; then
#  CUR_TIME=`cat /proc/uptime | awk '{printf "%.0f\n", $1}'`
#else
  # This will lead to inaccurate results but will prevent the script from breaking on platforms other than Linux
  CUR_TIME=`date +%s`
#fi

TMP=`mktemp -t .afl-whatsup-XXXXXXXX` || TMP=`mktemp -p /data/local/tmp .afl-whatsup-XXXXXXXX` || TMP=`mktemp -p /data/local/tmp .afl-whatsup-XXXXXXXX` || exit 1
trap "rm -f $TMP" 1 2 3 13 15

ALIVE_CNT=0
DEAD_CNT=0
START_CNT=0

TOTAL_TIME=0
TOTAL_EXECS=0
TOTAL_EPS=0
TOTAL_EPLM=0
TOTAL_CRASHES=0
TOTAL_HANGS=0
TOTAL_PFAV=0
TOTAL_PENDING=0
TOTAL_COVERAGE=

# Time since last find / crash / hang, formatted as string
FMT_TIME="0 days 0 hours"
FMT_FIND="${RED}none seen yet${NC}"
FMT_CRASH="none seen yet"
FMT_HANG="none seen yet"

if [ "$SUMMARY_ONLY" = "" ]; then
  
  echo "Individual fuzzers"
  echo "=================="
  echo
  
fi

fmt_duration()
{
  DUR_STRING=
  if [ $1 -le 0 ]; then
    return 1
  fi
  
  local duration=$((CUR_TIME - $1))
  local days=$((duration / 60 / 60 / 24))
  local hours=$(((duration / 60 / 60) % 24))
  local minutes=$(((duration / 60) % 60))
  local seconds=$((duration % 60))
  
  if [ $duration -le 0 ]; then
    DUR_STRING="0 seconds"
    elif [ $duration -eq 1 ]; then
    DUR_STRING="1 second"
    elif [ $days -gt 0 ]; then
    DUR_STRING="$days days, $hours hours"
    elif [ $hours -gt 0 ]; then
    DUR_STRING="$hours hours, $minutes minutes"
    elif [ $minutes -gt 0 ]; then
    DUR_STRING="$minutes minutes, $seconds seconds"
  else
    DUR_STRING="$seconds seconds"
  fi
}

FIRST=true
TOTAL_WCOP=
TOTAL_LAST_FIND=0

for j in `find . -maxdepth 2 -iname fuzzer_setup | sort`; do
  
  DIR=$(dirname "$j")
  i=$DIR/fuzzer_stats
  
  if [ -f "$i" ]; then
    
    IS_STARTING=
    IS_DEAD=
    sed 's/^command_line.*$/_skip:1/;s/[ ]*:[ ]*/="/;s/$/"/' "$i" >"$TMP"
    . "$TMP"
    DIRECTORY=$DIR
    DIR=${DIR##*/}
    RUN_UNIX=$run_time
    RUN_DAYS=$((RUN_UNIX / 60 / 60 / 24))
    RUN_HRS=$(((RUN_UNIX / 60 / 60) % 24))
    COVERAGE=$(echo $bitmap_cvg|tr -d %)
    if [ -n "$TOTAL_COVERAGE" -a -n "$COVERAGE" -a -n "$BC" ]; then
      if [ "$(echo "$TOTAL_COVERAGE < $COVERAGE" | bc)" -eq 1 ]; then
        TOTAL_COVERAGE=$COVERAGE
      fi
    fi
    if [ -z "$TOTAL_COVERAGE" ]; then TOTAL_COVERAGE=$COVERAGE ; fi
    
    test -n "$cycles_wo_finds" && {
      test -z "$FIRST" && TOTAL_WCOP="${TOTAL_WCOP}/"
      TOTAL_WCOP="${TOTAL_WCOP}${cycles_wo_finds}"
      FIRST=
    }
    
    if [ "$SUMMARY_ONLY" = "" ]; then
      
      echo ">>> $afl_banner instance: $DIR ($RUN_DAYS days, $RUN_HRS hrs) fuzzer PID: $fuzzer_pid <<<"
      echo
      
    fi
    
    if ! kill -0 "$fuzzer_pid" 2>/dev/null; then
      
      if [ -e "$i" ] && [ -e "$j" ] && [ -n "$FUSER" ]; then
        
        if [ "$i" -ot "$j" ]; then
          
          # fuzzer_setup is newer than fuzzer_stats, maybe the instance is starting?
          TMP_PID=`fuser -v "$DIRECTORY" 2>&1 | grep afl-fuzz`
          
          if [ -n "$TMP_PID" ]; then
            
            if [ "$SUMMARY_ONLY" = "" ]; then
              
              echo "  Instance is still starting up, skipping."
              echo
              
            fi
            
            START_CNT=$((START_CNT + 1))
            last_find=0
            IS_STARTING=1
            
            if [ "$PROCESS_DEAD" = "" ]; then
              
              continue
              
            fi
            
          fi
          
        fi
        
      fi
      
      if [ -z "$IS_STARTING" ]; then
        
        if [ "$SUMMARY_ONLY" = "" ]; then
          
          echo "  Instance is dead or running remotely, skipping."
          echo
          
        fi
        
        DEAD_CNT=$((DEAD_CNT + 1))
        IS_DEAD=1
        last_find=0
        
        if [ "$PROCESS_DEAD" = "" ]; then
          
          continue
          
        fi
        
      fi
      
    fi
    
    ALIVE_CNT=$((ALIVE_CNT + 1))
    
    EXEC_SEC=0
    EXEC_MIN=0
    test -z "$RUN_UNIX" -o "$RUN_UNIX" = 0 || EXEC_SEC=$((execs_done / RUN_UNIX))
    PATH_PERC=$((cur_item * 100 / corpus_count))

    test "$IS_DEAD" = 1 || EXEC_MIN=$(echo $execs_ps_last_min|sed 's/\..*//')
    
    TOTAL_TIME=$((TOTAL_TIME + RUN_UNIX))
    TOTAL_EPS=$((TOTAL_EPS + EXEC_SEC))
    TOTAL_EPLM=$((TOTAL_EPLM + EXEC_MIN))
    TOTAL_EXECS=$((TOTAL_EXECS + execs_done))
    TOTAL_CRASHES=$((TOTAL_CRASHES + saved_crashes))
    TOTAL_HANGS=$((TOTAL_HANGS + saved_hangs))
    TOTAL_PENDING=$((TOTAL_PENDING + pending_total))
    TOTAL_PFAV=$((TOTAL_PFAV + pending_favs))
    
    if [ "$last_find" -gt "$TOTAL_LAST_FIND" ]; then
      TOTAL_LAST_FIND=$last_find
    fi
    
    if [ "$SUMMARY_ONLY" = "" ]; then
      
      # Warnings in red
      TIMEOUT_PERC=$((exec_timeout * 100 / execs_done))
      if [ $TIMEOUT_PERC -ge 10 ]; then
        echo "  ${RED}timeout_ratio $TIMEOUT_PERC%${NC}"
      fi
      
      if [ $EXEC_SEC -eq 0 ]; then
        echo "  ${YELLOW}no data yet, 0 execs/sec${NC}"
        elif [ $EXEC_SEC -lt 100 ]; then
        echo "  ${RED}slow execution, $EXEC_SEC execs/sec${NC}"
      fi
      
      fmt_duration $last_find && FMT_FIND=$DUR_STRING
      fmt_duration $last_crash && FMT_CRASH=$DUR_STRING
      fmt_duration $last_hang && FMT_HANG=$DUR_STRING
      FMT_CWOP="not available"
      test -n "$cycles_wo_finds" && {
        test "$cycles_wo_finds" = 0 && FMT_CWOP="$cycles_wo_finds"
        test "$cycles_wo_finds" -gt 10 && FMT_CWOP="${YELLOW}$cycles_wo_finds${NC}"
        test "$cycles_wo_finds" -gt 50 && FMT_CWOP="${RED}$cycles_wo_finds${NC}"
      }
      
      echo "  last_find       : $FMT_FIND"
      echo "  last_crash      : $FMT_CRASH"
      if [ -z "$MINIMAL_ONLY" ]; then
        echo "  last_hang       : $FMT_HANG"
        echo "  cycles_wo_finds : $FMT_CWOP"
      fi
      echo "  coverage        : $COVERAGE%"
      
      if [ -z "$MINIMAL_ONLY" ]; then
        
        CPU_USAGE=$(ps aux | grep -w $fuzzer_pid | grep -v grep | awk '{print $3}')
        MEM_USAGE=$(ps aux | grep -w $fuzzer_pid | grep -v grep | awk '{print $4}')
        
        echo "  cpu usage $CPU_USAGE%, memory usage $MEM_USAGE%"
        
      fi
      
      echo "  cycles $((cycles_done + 1)), lifetime speed $EXEC_SEC execs/sec, items $cur_item/$corpus_count (${PATH_PERC}%)"
      
      if [ "$saved_crashes" = "0" ]; then
        echo "  pending $pending_favs/$pending_total, coverage $bitmap_cvg, no crashes yet"
      else
        echo "  pending $pending_favs/$pending_total, coverage $bitmap_cvg, crashes saved $saved_crashes (!)"
      fi
      
      echo
      
    fi

  else

    if [ ! -e "$i" -a -e "$j" ]; then

      if [ '!' "$PROCESS_DEAD" = "" ]; then
        ALIVE_CNT=$((ALIVE_CNT + 1))
      fi
      START_CNT=$((START_CNT + 1))
      last_find=0
      IS_STARTING=1
      
    fi

  fi
  
done

# Formatting for total time, time since last find, crash, and hang
fmt_duration $((CUR_TIME - TOTAL_TIME)) && FMT_TIME=$DUR_STRING
# Formatting for total execution
FMT_EXECS="0 millions"
EXECS_MILLION=$((TOTAL_EXECS / 1000 / 1000))
EXECS_THOUSAND=$((TOTAL_EXECS / 1000 % 1000))
if [ $EXECS_MILLION -gt 9 ]; then
  FMT_EXECS="$EXECS_MILLION millions"
  elif [ $EXECS_MILLION -gt 0 ]; then
  FMT_EXECS="$EXECS_MILLION millions, $EXECS_THOUSAND thousands"
else
  FMT_EXECS="$EXECS_THOUSAND thousands"
fi

rm -f "$TMP"

TOTAL_DAYS=$((TOTAL_TIME / 60 / 60 / 24))
TOTAL_HRS=$(((TOTAL_TIME / 60 / 60) % 24))

test -z "$TOTAL_WCOP" && TOTAL_WCOP="not available"
fmt_duration $TOTAL_LAST_FIND && TOTAL_LAST_FIND=$DUR_STRING

test "$TOTAL_TIME" = "0" && TOTAL_TIME=1

if [ "$PROCESS_DEAD" = "" ]; then
  
  TXT="excluded from stats"
  
else
  
  TXT="included in stats"
  ALIVE_CNT=$(($ALIVE_CNT - $DEAD_CNT - $START_CNT))
  
fi

echo "Summary stats"
echo "============="
if [ -z "$SUMMARY_ONLY" -o -z "$MINIMAL_ONLY" ]; then
  echo
fi

echo "        Fuzzers alive : $ALIVE_CNT"

if [ ! "$START_CNT" = "0" ]; then
  echo "          Starting up : $START_CNT ($TXT)"
fi

if [ ! "$DEAD_CNT" = "0" ]; then
  echo "       Dead or remote : $DEAD_CNT ($TXT)"
fi

echo "       Total run time : $FMT_TIME"
if [ -z "$MINIMAL_ONLY" ]; then
  echo "          Total execs : $FMT_EXECS"
  echo "     Cumulative speed : $TOTAL_EPS execs/sec"
  if [ "$ALIVE_CNT" -gt "0" ]; then
    echo "  Total average speed : $((TOTAL_EPS / ALIVE_CNT)) execs/sec"
  fi
fi
if [ "$ALIVE_CNT" -gt "0" ]; then
  echo "Current average speed : $TOTAL_EPLM execs/sec"
fi
if [ -z "$MINIMAL_ONLY" ]; then
  echo "        Pending items : $TOTAL_PFAV faves, $TOTAL_PENDING total"
fi

if [ "$ALIVE_CNT" -gt "1" -o -n "$MINIMAL_ONLY" ]; then
  if [ "$ALIVE_CNT" -gt "0" ]; then
    echo "   Pending per fuzzer : $((TOTAL_PFAV/ALIVE_CNT)) faves, $((TOTAL_PENDING/ALIVE_CNT)) total (on average)"
  fi
fi

echo "     Coverage reached : ${TOTAL_COVERAGE}%"
echo "        Crashes saved : $TOTAL_CRASHES"
if [ -z "$MINIMAL_ONLY" ]; then
  echo "          Hangs saved : $TOTAL_HANGS"
  echo " Cycles without finds : $TOTAL_WCOP"
fi
echo "   Time without finds : $TOTAL_LAST_FIND"
echo

exit 0
